Skip to content

feat: transfer to spending from hw wallet#1039

Merged
ovitrif merged 30 commits into
masterfrom
feat/hw-transfer-to-spending
Jun 24, 2026
Merged

feat: transfer to spending from hw wallet#1039
ovitrif merged 30 commits into
masterfrom
feat/hw-transfer-to-spending

Conversation

@ovitrif

@ovitrif ovitrif commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #1028
Refs #998
Refs #1029

This PR adds the watch-only hardware-wallet Transfer to Spending flow for funding a Lightning channel from a paired Trezor with the on-chain transaction signed on the device. Built on top of #1033.

Description

  • Adds the four-step flow Amount → Sign With Your Device → Transaction Signed → Funds In Transfer, entered from the hardware wallet detail screen's Transfer To Spending button.
  • Shows the shared SpendingIntro before the first hardware-wallet transfer to spending, then marks the shared intro as seen and continues to the hardware-wallet amount flow.
  • Sources the available amount and MAX from the device's native-segwit account balance, minus the final order.feeSat and the composed on-chain mining fee, while respecting the LSP receiving-cap limit.
  • Composes the hardware-wallet funding transaction before signing, signs it on the Trezor, broadcasts it, then stores the actual composed mining fee, fee rate, and total spent on the transfer activity.
  • Confirms hardware-wallet transfer activities from watcher data, preserving the existing transfer metadata while updating confirmation details.
  • Prevents the total balance from double-counting during the paid-order → pending-channel → ready-channel transition by matching active hardware-wallet transfers to channel funding outpoints.
  • Reuses the existing transfer machinery: the spending amount input, Blocktank order, channel watcher, and the Funds In Transfer progress screen, swapping only local signing for on-device signing.
  • Hardens Trezor reconnect/signing reliability by separating reconnect, compose, sign, and broadcast phases, reusing sessions when possible, and keeping long-running Bridge signing calls alive while the device waits for user confirmation.
  • Keeps the Sign screen's Learn More and Advanced controls working by reusing the existing liquidity-info and LSP-balance advanced screens.
  • v1 funds from the native-segwit account only; multi-address-type spend is out of scope. Transfer to Savings for these funds is tracked separately.

Figma Designs:

Preview

Flow + Trezor Polished UI
hwtransfer2x.mp4
max2x.mp4

QA Notes

Verified the original happy path end-to-end on an Android Pixel Phone against Trezor Bridge and a physical Trezor Safe 7 on regtest: the funding tx was signed on-device, broadcast, and the channel opened, moving the entered amount into the spending balance.

Additional Trezor Dev Bridge validation after Piotr's first-round review covered MAX, watcher confirmation, actual mining-fee persistence, and balance behavior around the paid-order/in-transfer phase. Trezor Dev Bridge signed and broadcast tx 7da3bf70153d47d1c1075ceb248b15c561ba5454c8a0d501d89877fec7cb17fa, then mined it to 6 confirmations. The transfer activity row ended confirmed=1, value=43045, fee=141, fee_rate=1, and is_transfer=1.

Hosted staging Blocktank validation covered the final paid-order → pending-channel → ready-channel transition using the LSP regtest API. Order 3e24ba9c-43d3-4b44-acbd-c8cf1c42ade4 moved to paid, then executed; channel a2ed4674668e74a92e55cf7e4780a13dadafbd81acd6a6d6e8f39d80b7856d4b became ready from funding outpoint 4b6d85b7809df3e8d6a6d6ac81bdafad3da180477ecf552ea9748e667446eda2:0. The app persisted that channel for the active transfer, marked the HW activity as transfer metadata for that channel, settled the transfer, and the final balance state showed balanceInTransferToSpending=0 with totalLightningSats=562194.

Manual Tests

  • 1. Hardware Wallet detail → Transfer To Spending → Amount: shows TRANSFER TO SPENDING, AVAILABLE from the device balance, and the 25% / MAX / unit-toggle controls.
  • 2. Amount → 25% or MAX → Continue → Sign: a Blocktank order is created and Sign shows SIGN screen with the detailed fees.
  • 3. Sign → Open Trezor Connect: composes and signs on the device, broadcasts, advances to Transaction Signed, then auto-forwards to Funds In Transfer; spending increases and the hardware wallet balance drops by the signed send.
  • 4. MAX with a low LSP receiving cap: AVAILABLE is capped by the receivable amount, the signed output equals final order.feeSat, and the wallet spends only order.feeSat + composed mining fee.
  • 5. After the funding tx confirms: the transfer activity confirms from watcher data and shows the actual composed mining fee.
  • 6. Total Balance during state transitions (paid-order → pending-channel → ready-channel) does not double count the transfer amount while the pending channel appears.
  • 7. BLE reconnect
    • 7a. if unlocked → Open Trezor Connect: silently reconnects, signs, and broadcasts.
    • 7b. if locked → unlock device, retry, then sign and broadcast without losing the order.
    • 7c. on failure → Open Trezor Connect: shows the Reconnect Hardware Device error.
  • 8. USB Bridge signing: a long device-confirmation pause does not time out before the user approves on the Trezor.
  • 9. Amount above the limit: input resets to MAX and shows the max-amount error toast.
  • 10. Insufficient funds for order.feeSat + composed mining fee: signing is blocked with the insufficient-funds state instead of broadcasting.
  • 11a. Sign → Learn More: opens the liquidity info screen.
  • 11b. Sign → Advanced → adjust receiving balance: returns to Sign with updated fees.

Automated Checks

  • Automated tests added/updated:
    • ActivityRepoTest.kt: watcher confirmations update existing hardware-wallet transfer activities while preserving transfer metadata.
    • ContentViewTest.kt: first-time hardware-wallet transfer-to-spending starts at SpendingIntro, preserves the hardware device id for the HW amount route, and skips the intro once it has been seen.
    • DeriveBalanceStateUseCaseTest.kt: active hardware-wallet transfers are not double-counted when LDK exposes the pending channel before Blocktank order metadata catches up.
    • HwWalletRepoTest.kt: native-segwit funding balance is tracked separately from aggregate hardware-wallet balance, and hardware-wallet funding compose/sign/broadcast returns and persists the composed mining fee, fee rate, and total spent.
    • TransferRepoTest.kt: paid-order/channel lookup handles hardware-wallet funding transaction outpoints during settlement transitions.
    • TransferViewModelTest.kt: HW MAX/available math, fee-estimator fallback reserve, fallback compose fee rate, final order.feeSat funding, reconnect-on-disconnect, stale-session cleanup on signing timeout, and sign/broadcast error states.
    • TrezorRepoTest.kt: reconnect/session handling around hardware-wallet signing, reconnect cancellation propagation with isConnecting cleanup, and reset fully disconnects connected transport sessions.
    • TrezorBridgeTransportTest.kt: Bridge /call/{session} uses the longer signing read timeout while management/non-signing requests keep the shorter timeout, and Bridge HTTP calls honor the selected timeout.
  • Test journeys added/updated:
    • transfer-to-spending.xml
    • transfer-to-spending-max-lsp-cap.xml
    • transfer-to-spending-node-warmup.xml
    • detail-overview.xml
  • E2E AI test on Trezor Dev Bridge:
    • Installed the patched dev app, funded the emulator hardware wallet with 3,000,000 sats, selected MAX with LSP-capped AVAILABLE of 41,923 sats, signed/broadcast tx 7da3bf70153d47d1c1075ceb248b15c561ba5454c8a0d501d89877fec7cb17fa, mined 6 blocks, verified activity confirmation and actual fee persistence, and confirmed total balance moved only by Blocktank fees plus the 141 sat composed mining fee.
  • E2E AI test on Blocktank staging:
    • Used ./lsp POST /regtest/chain/deposit, ./lsp POST /regtest/chain/mine, and ./lsp POST /channels/:id/open to drive the paid → executed → ready-channel transition, then verified the app settled the active transfer and removed the in-transfer balance.
  • checks: just test file ContentViewTest, just test file TransferViewModelTest, just test file DeriveBalanceStateUseCaseTest, just test file TrezorRepoTest, just compile, just test, just lint.

@ovitrif ovitrif self-assigned this Jun 22, 2026
@ovitrif ovitrif added this to the 2.4.0 milestone Jun 22, 2026
@ovitrif ovitrif changed the title feat: transfer to spending from hardware wallet feat: transfer to spending from hw wallet Jun 22, 2026
Base automatically changed from feat/hw-wallet-connect to master June 22, 2026 21:48
@ovitrif ovitrif force-pushed the feat/hw-transfer-to-spending branch from 722a6bd to 455eb68 Compare June 22, 2026 22:25
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Observations from initial testing

Below I attach AI analysis of the issues: HW transfer to spending — QA analysis

1. transfer from Trezor 7 connected via BLE

  • my initial balance on Trezor was 2 100 000
  • had issue each time (2 or 3 times) when trying to transfer MAX - was able to sign transaction but at the end got "Reconnect Hardware Device" error toast ❌
Screenshot_20260623_112745_Bitkit Regtest
  • after those few failed attampts I tried to transfer 25% of Trezor balance and was successful with that ✅
  • Tried to transfer again from Trezor - had 1 682 648 balance on Trezor - but calculated MAX on Transfer to spending was 90 278 - which looked a bit off [but guessing it was due to LSP liquidity limits] ⚠️
  • during the transfer I had Bitkit/Trezor enforce reconnect device - despite the fact it was connected before - this operation again ended with "Reconnect Hardware Device" toast error. ❌
Screen_Recording_20260623_120550_Bitkit.Regtest.mp4

2. transfer from Trezor 7 connected via USB

  • after that I disconnected Trezor 7 connected via BLE and connected the same device using USB.
  • I was able to send MAX twice - which calculated as ~90k both times with the 1500k balance on trezor.
  • After that the Transfer to spending shown 0 for me and wasn't able to transfer anymore. ⚠️
  • Closed all the channels, mined 10 blocks, tried to transfer MAX. Got "Insufficient funds" error toast ❌
Screen_Recording_20260623_130312_Bitkit.Regtest.mp4

Attaching logs from the session:
bitkit_logs_2026-06-23_11-06-01.zip

In general it looks that there is a lot of "reconnect" errors - especially on connection via BLE.
Also some polishing around LSP limits is probably needed - last error showing insufficient funds.

Other observations:

  • Looks like "Transfer" transactions that are from Trezor never get "Confirmed" status and are permanently "In Transfer" despite the blocks were they belong are mined long time ago. ❌
Screen.Recording.2026-06-23.at.11.59.26.mov

HW transfer to spending — QA analysis

Session logs: bitkit_logs_2026-06-23_11-06-01 (bitkit-logs)
Device: Samsung S22, Android 16, Trezor Safe 7, regtest
App: 2.3.0 (182)

Cross-checked manual testing observations against logs and the #1039 transfer flow.


Verdict overview

Observation Verdict Severity
BLE “Reconnect” after signing MAX Bug — mostly 45s sign timeouts, not disconnect Blocker
MAX ~90k with ~1.5M+ on Trezor Expected — LSP total liquidity cap UX / polish
MAX → 0 after two ~90k USB transfers Expected — cap exhausted UX / polish
“Insufficient funds” after closing channels Bug — MAX vs order.feeSat / compose High
Trezor transfers stuck “In Transfer” Bug — no confirmation sync for HW txs Blocker

1. BLE — “Reconnect Hardware Device” after MAX

Logs: Four failed MAX attempts used clientBalanceSat ≈ 1,661,647 (not ~90k). Failures are:

  • signTxFromPsbt failed [TimeoutCancellationException: Timed out waiting for 45000 ms] (×3)
  • connectKnownDevice timeout (×1)
  • One user cancel: Device error (code 4): Cancelled

All timeouts surface the same “Reconnect Hardware Device” toast, even though BLE was still active (244-byte TX/RX right before timeout).

Successful BLE transfer: 25% order (415,412 sats) at 09:33 — tx af815b34…, order settled.

Recommendations

  1. Don’t use the reconnect toast for timeouts — separate copy (“Signing took too long…”) and check mempool/watcher before erroring (tx may have broadcast).
  2. Raise BLE sign timeout (45s is tight for large PSBTs); consider separate budgets for reconnect vs sign.
  3. Skip forceSession disconnect when already connected on the same deviceId.
  4. Retest MAX on BLE only after smaller amounts work; first MAX at ~1.66M is a stress case.

Also saw a real BLE failure: THP handshake failed: Expected channel allocation response, got: 3f (~10:56) — same reconnect toast; needs distinct handling.


2. MAX ~90k with large Trezor balance

Expected, not a Trezor balance bug.

MAX is capped by Blocktank total channel liquidity, not Trezor balance. Formula subtracts existing Blocktank channel capacity from LSP maxChannelSizeSat (~1.74M on regtest).

Logs: USB success at 10:17 — Trezor ~1.68M, transfer 90,278 sats (amountSats=90278). LDK already had large claimable closes (~415k + others), so remaining headroom was ~90k.

Recommendations

  • UI: distinguish Trezor available vs LSP transfer limit (MAX label / helper text).
  • When MAX is tiny vs Trezor balance, explain that spending from Lightning does not free this cap — only coop-close / transfer to savings does.

3. USB — two ~90k MAX, then MAX = 0

Expected after ~415k (BLE) + 2× ~90k (USB) ≈ ~1.6M of ~1.74M cap.

Sending from spending balance does not reduce existing_channels_total_sat; only closing channels does.


4. “Insufficient funds” after closing channels

Bug — misleading MAX, not empty Trezor.

After coop-close, snapshot shows 0 channels, 483k on-chain swept, Trezor watcher still ~1.5M. UI offered MAX with clientBalanceSat ≈ 1,498,181.

Logs (11:01:54):

Insufficient funds: 1499674 sat available of 1500672 sat needed998 sats short.

MAX is derived from watcher total balance and client amount; signing pays order.feeSat (client + LSP/network fees). Compose also had to select multiple UTXOs after several HW sends.

Recommendations

  1. MAX must reserve order.feeSat (not just clientBalanceSat) + mining fee slack.
  2. Optional compose dry-run before sign screen.
  3. User-facing error: “Not enough confirmed Trezor balance for this order (fees included)” instead of generic “Insufficient funds”.
  4. Refresh watcher before sign after channel closes.

5. Trezor “Transfer” activities stuck “In Transfer”

Confirmed bug.

Successful HW paths log Created sent onchain activity … confirmed=false. Orders settle (Order settled in logs), but activities never flip confirmed because HW txs are outside LDK — only the Trezor watcher sees confirmations; main activity store is not updated.

Recommendation

When watcher reports confirmations > 0 (or order is EXECUTED), upsert main activity confirmed=true by txid.


PR #1039 — suggested gate

Blockers before merge

  • Reconnect toast conflates timeout / THP failure / real disconnect (especially BLE).
  • HW transfer activities never confirm in Activity list.
  • Insufficient funds when MAX looked valid (fee / order.feeSat math).

Polish (non-blocking but worth tracking)

  • LSP limit UX (MAX vs Trezor balance, MAX=0 messaging).
  • Power save was on in support snapshot — may worsen BLE; note in QA matrix.

Looks correct / expected

  • ~90k MAX with large Trezor balance.
  • MAX → 0 after filling LSP capacity.

Log references (for dev follow-up)

Event Time (UTC log) Detail
Failed MAX order (BLE) 09:20:34 clientBalanceSat=1661647
Sign timeout 09:22:36, 09:24:06, 09:27:41 ble:74:40:5F:E1:14:71 / ble:47:AD:A6:65:A3:BA
Successful 25% (BLE) 09:33:02 tx af815b34…, order b71f49f9-b977-41d1-b727-5c2f9c2f57b7
USB ~90k success 10:17:34 tx 0c296c4…, amountSats=90278
THP handshake fail ~10:56:49 Expected channel allocation response, got: 3f
Insufficient funds 11:01:54, 11:03:06 1499674 available of 1500672 needed

@ovitrif

ovitrif commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

had issue each time (2 or 3 times) when trying to transfer MAX - was able to sign transaction but at the end got "Reconnect Hardware Device" error toast

"Insufficient funds" after closing channels

Looks like "Transfer" transactions that are from Trezor never get "Confirmed" status and are permanently "In Transfer"

Thanks Piotr, I pushed fixes for the issues you identified in the first round of early validation.

This now:

  • confirms HW transfer activities from Trezor watcher data
  • avoids double-counting total balance while a paid HW order becomes a pending/ready channel
  • calculates HW funding/MAX against the final order.feeSat plus the composed mining fee
  • stores the actual composed mining fee on the HW transfer activity
  • separates reconnect, funding composition, and signing timeout handling, with better BLE/session reuse

Validation video: MAX transfer

max2x.mp4

Resolved in 2ecf4d1

@ovitrif ovitrif marked this pull request as ready for review June 24, 2026 00:40
@greptile-apps

This comment has been minimized.

greptile-apps[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

QA re-test — HW transfer to spending (2026-06-24)

Build: 2.3.0 (182) · Samsung S22, Android 16 · regtest · Trezor Safe 7
Logs:
bitkit_logs_2026-06-24_10-26-57.zip

What I tested

  • Trezor → spending on BLE and USB
  • MAX at LSP cap (~1.65M sats) — both transports succeeded
  • Smaller transfers, multiple channel opens (6 settled orders), coop closes (incl. batch close), spending → new channel (~138k)
  • Pair / forget / re-pair BLE; BLE ↔ USB switching
  • Activity list — HW transfers no longer stuck “In Transfer”; confirm as expected after completion (fixes prior QA blocker)
  • Mostly tested with power save on. Sanity tested with power save off: connect via BLE + transfer to spending

Result

Stable — no sign timeouts, no insufficient-funds on MAX, no THP failures on successful paths (major improvement vs 2026-06-23 session).

Minor UX (non-blocking)

On the HW sign screen, tapping “Open Trezor Connect” sometimes results in red “Reconnect Hardware Device” toast when device is locked / connection in progress — usually fixed by unlocking Trezor, not re-pairing. Logs: THP Error: DeviceLocked, Connection already in progress. Softer copy would help.

Out of scope for this PR (filed separately)

Verdict

Approve for HW transfer-to-spending scope. Manual coverage is sufficient for merge; follow-ups are tracked issues + toast polish.

piotr-iohk
piotr-iohk previously approved these changes Jun 24, 2026

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK 👍

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Noticed additional minor issue — SpendingIntro / hasSeenSpendingIntro

SpendingIntro is skipped on HW → spending and also hasSeenSpendingIntro is never set, so intro still appears on later savings → spending transfer despite HW -> spending was done already.

Screenshot 2026-06-24 at 13 35 16

Comment thread app/src/main/java/to/bitkit/models/HardwareWallet.kt
Comment thread app/src/main/java/to/bitkit/repositories/HwWalletRepo.kt
Comment thread app/src/main/java/to/bitkit/repositories/TrezorRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt Outdated
@ovitrif

ovitrif commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator Author

Noticed additional minor issue — SpendingIntro / hasSeenSpendingIntro

SpendingIntro is skipped on HW → spending and also hasSeenSpendingIntro is never set, so intro still appears on later savings → spending transfer despite HW -> spending was done already.

Fixed by routing first-time hardware-wallet transfer-to-spending through the same shared SpendingIntro gate. Continuing from that intro now sets hasSeenSpendingIntro=true and then opens the hardware-wallet amount screen with the selected deviceId; once the intro has been seen, both HW and app-wallet transfer-to-spending flows skip it.

I also checked the supplied Figma HW frames: they start at the HW Amount screen and do not define a separate HW intro, so this reuses the existing generic spending intro.

Resolved in d9a5314

piotr-iohk
piotr-iohk previously approved these changes Jun 24, 2026

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approve after SpendingIntro fix.

Comment thread app/src/main/java/to/bitkit/repositories/TrezorRepo.kt

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utAck

@ovitrif ovitrif enabled auto-merge June 24, 2026 16:36
@ovitrif ovitrif merged commit c25359e into master Jun 24, 2026
17 checks passed
@ovitrif ovitrif deleted the feat/hw-transfer-to-spending branch June 24, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transfer to Spending from HW device flow

3 participants